package com.benasher44.uuid

import java.nio.ByteBuffer
import java.util.UUID

// This suppression is required to force Kotlin to accept the Java getters as
// val definitions:
//
// - https://youtrack.jetbrains.com/issue/KT-15620
// - https://youtrack.jetbrains.com/issue/KT-27413
//@Suppress("NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS")
//public typealias Uuid1  = UUID

public actual class Uuid
@Deprecated("Use `uuidOf` instead.", ReplaceWith("uuidOf(uuid)"))
constructor(internal val uuidBytes: ByteArray) : Comparable<Uuid> {
    public actual val mostSignificantBits: Long
        get() = bytesToLong(uuidBytes, 0)

    public actual val leastSignificantBits: Long
        get() = bytesToLong(uuidBytes, 8)
    private val delegate: UUID = UUID(mostSignificantBits, leastSignificantBits)

    public actual constructor(msb: Long, lsb: Long) : this(longsToBytes(msb, lsb))

    public fun asJavaUUID(): UUID = delegate

    public companion object {
        public fun fromJavaUUID(uuid: UUID): Uuid {
            return Uuid(uuid.mostSignificantBits, uuid.leastSignificantBits)
        }
    }

    actual override fun compareTo(other: Uuid): Int {
        return when {
            mostSignificantBits != other.mostSignificantBits -> {
                if (mostSignificantBits < other.mostSignificantBits) -1 else 1
            }
            leastSignificantBits != other.leastSignificantBits -> {
                if (leastSignificantBits < other.leastSignificantBits) -1 else 1
            }
            else -> 0
        }
    }
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Uuid) return false
        return mostSignificantBits == other.mostSignificantBits &&
                leastSignificantBits == other.leastSignificantBits
    }

    override fun hashCode(): Int {
        return (mostSignificantBits xor (mostSignificantBits.ushr(32))).toInt() xor
                (leastSignificantBits xor (leastSignificantBits.ushr(32))).toInt()
    }
    override fun toString(): String = delegate.toString()
}

private fun bytesToLong(bytes: ByteArray, offset: Int): Long {
    var result = 0L
    for (i in 0 until 8) {
        result = result shl 8
        result = result or (bytes[offset + i].toLong() and 0xFF)
    }
    return result
}
private fun longsToBytes(msb: Long, lsb: Long): ByteArray {
    val bytes = ByteArray(16)
    var value = msb
    for (i in 7 downTo 0) {
        bytes[7 - i] = (value and 0xFF).toByte()
        value = value shr 8
    }
    value = lsb
    for (i in 7 downTo 0) {
        bytes[15 - i] = (value and 0xFF).toByte()
        value = value shr 8
    }
    return bytes
}

public actual fun uuidOf(bytes: ByteArray): Uuid {
    require(bytes.size == UUID_BYTES) {
        "Invalid UUID bytes. Expected $UUID_BYTES bytes; found ${bytes.size}"
    }

    val msb = bytesToLong(bytes, 0)
    val lsb = bytesToLong(bytes, 8)

    return Uuid(msb, lsb)
}

// Implementation Notes:
// - This algorithm could be implemented in the common module and reused for all
//   targets. However, using two longs is not necessarily the best performing
//   solution in other targets. JavaScript for instance does not have native
//   support for 64 bit integers and they are always wrapped in an object
//   (similar to a BigDecimal in Java), a org.khronos.webgl.Uint8ClampedArray is
//   probably the best choice there. Native targets are probably better at
//   dealing with byte arrays, or even directly using native functions… only
//   careful benchmarking can tell and UUIDs are ALWAYS in the critical path of
//   applications, hence, we want to have the best solution for each target.
// - We are not using any constants but rather literal values because constants
//   lead to code being generated that is accessible via reflection, but we do
//   not want to leak anything.
// - The length verification at the beginning does not always lead to the
//   exception being thrown because length is UTF-16 based and not byte based as
//   it is actually required here. Careful benchmarking, however, showed that
//   any kind of conversion leads to a loss in throughput that is not worth it
//   to pay since the hex-conversion that follows is going to catch all chars
//   that are not valid hexadecimal digits anyways.
// - Regarding benchmarks, the following numbers are from a Zulu OpenJDK 13 run
//   between java.util.UUID and this implementation on a 2017 MacBook Pro with
//   Catalina:
//
//   Benchmark                    Mode  Cnt         Score        Error  Units
//   UuidParserBenchmark.java    thrpt   25   9628223.641 ± 248719.696  ops/s
//   UuidParserBenchmark.kotlin  thrpt   25  13948372.928 ± 694944.523  ops/s
//
//   Other strategies (e.g. ByteArray, ByteBuffer, …) are all slower than the
//   MSB/LSB approach. The reason for that has probably to do with how the JVM
//   works and that such arrays always lead to heap allocations.
public actual fun uuidFrom(string: String): Uuid =
    if (string.length == 36) {
        Uuid(string.segmentToLong(0, 19), string.segmentToLong(19, 36))
    } else {
        throw IllegalArgumentException(
            "Invalid UUID string, expected exactly 36 characters but got ${string.length}: $string",
        )
    }

private fun String.segmentToLong(start: Int, end: Int): Long {
    var result = 0L

    var i = start
    do {
        if (this[i] == '-') {
            require(i == 8 || i == 13 || i == 18 || i == 23) {
                "Invalid UUID string, encountered dash at index $i but it can occur only at 8, 13, 18, or 23: $this"
            }
        } else {
            result *= 16
            when (this[i]) {
                '0' -> Unit
                '1' -> result += 1L
                '2' -> result += 2L
                '3' -> result += 3L
                '4' -> result += 4L
                '5' -> result += 5L
                '6' -> result += 6L
                '7' -> result += 7L
                '8' -> result += 8L
                '9' -> result += 9L
                'a', 'A' -> result += 10L
                'b', 'B' -> result += 11L
                'c', 'C' -> result += 12L
                'd', 'D' -> result += 13L
                'e', 'E' -> result += 14L
                'f', 'F' -> result += 15L
                else -> throw IllegalArgumentException(
                    "Invalid UUID string, encountered non-hexadecimal digit `${this[i]}` at index $i in: $this",
                )
            }
        }
    } while (++i < end)

    return result
}

@Suppress("NOTHING_TO_INLINE")
public actual inline fun uuid4(): Uuid {
    return Uuid.fromJavaUUID(UUID.randomUUID())
}

public actual val Uuid.bytes: ByteArray
    get() {
        return ByteBuffer.allocate(16)
            .putLong(mostSignificantBits)
            .putLong(leastSignificantBits)
            .array()
    }

public actual val Uuid.version: Int
    get() = when {
        mostSignificantBits and 0x000000000000F000L == 0x0000000000001000L -> 1
        mostSignificantBits and 0x000000000000F000L == 0x0000000000002000L -> 2
        mostSignificantBits and 0x000000000000F000L == 0x0000000000003000L -> 3
        mostSignificantBits and 0x000000000000F000L == 0x0000000000004000L -> 4
        mostSignificantBits and 0x000000000000F000L == 0x0000000000005000L -> 5
        else -> -1
    }

private val VARIANT_NCS_MASK = 0x8000_0000_0000_0000UL
private val VARIANT_RFC_MASK = 0xC000_0000_0000_0000UL
private val VARIANT_MS_MASK  = 0xE000_0000_0000_0000UL

public actual val Uuid.variant: Int
    get() {
        val lsb = leastSignificantBits.toULong()
        return when {
            (lsb and VARIANT_NCS_MASK) == 0UL -> 0
            (lsb and VARIANT_RFC_MASK) == VARIANT_NCS_MASK -> 2
            (lsb and VARIANT_MS_MASK)  == VARIANT_RFC_MASK -> 6
            else -> 7
        }
    }